﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;
using System.Windows;
using System.ComponentModel;

namespace Utils.Wpf
{
	public enum DateTimePickerFormat { Long, Short, Time, Custom }

	[System.ComponentModel.DefaultBindingProperty("Value")]

	public class TimePicker : Control
	{
		private CheckBox _checkBox;
		internal TextBox _textBox;
		private TextBlock _textBlock;
		private Popup _popUp;
		private Calendar _calendar;
		private BlockManager _blockManager;
		private const string _defaultFormat = "MM/dd/yyyy hh:mm:ss tt";

		[Category("TimePicker2")]
		public bool ShowCheckBox
		{
			get { return _checkBox.Visibility == System.Windows.Visibility.Visible; }
			set
			{
				if (value)
					_checkBox.Visibility = System.Windows.Visibility.Visible;
				else
				{
					_checkBox.Visibility = System.Windows.Visibility.Collapsed;
					this.Checked = true;
				}
			}
		}

		[Category("TimePicker2")]
		public bool ShowDropDown
		{
			get { return _textBlock.Visibility == System.Windows.Visibility.Visible; }
			set
			{
				if (value)
					_textBlock.Visibility = System.Windows.Visibility.Visible;
				else
					_textBlock.Visibility = System.Windows.Visibility.Collapsed;
			}
		}

		[Category("TimePicker2")]
		public bool Checked
		{
			get { return _checkBox.IsChecked.HasValue && _checkBox.IsChecked.Value; }
			set { _checkBox.IsChecked = value; }
		}

		[Category("TimePicker2")]
		private string FormatString
		{
			get
			{
				switch (this.Format)
				{
					case DateTimePickerFormat.Long:
						return "dddd, MMMM dd, yyyy";
					case DateTimePickerFormat.Short:
						return "M/d/yyyy";
					case DateTimePickerFormat.Time:
						return "HH:mm:ss";
					case DateTimePickerFormat.Custom:
						if (string.IsNullOrEmpty(this.CustomFormat))
							return _defaultFormat;
						else
							return this.CustomFormat;
					default:
						return _defaultFormat;
				}
			}
		}

		private string _customFormat;
		[Category("TimePicker2")]
		public string CustomFormat
		{
			get { return _customFormat; }
			set
			{
				_customFormat = value;
				_blockManager = new BlockManager(this, this.FormatString);
			}
		}

		private DateTimePickerFormat _format;
		[Category("TimePicker2")]
		public DateTimePickerFormat Format
		{
			get { return _format; }
			set
			{
				_format = value;
				_blockManager = new BlockManager(this, this.FormatString);
			}
		}

		[Category("TimePicker2")]
		public DateTime? Value
		{
			get
			{
				if (!this.Checked) return null;
				return (DateTime?)GetValue(ValueProperty);
			}
			set { SetValue(ValueProperty, value); }
		}

		// Using a DependencyProperty as the backing store for TheDate.  This enables animation, styling, binding, etc...
		public static readonly DependencyProperty ValueProperty =
			DependencyProperty.Register("Value", typeof(DateTime?), typeof(TimePicker), new FrameworkPropertyMetadata(DateTime.Now, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, TimePicker.OnValueChanged, TimePicker.CoerceValue, true, System.Windows.Data.UpdateSourceTrigger.PropertyChanged));


		static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			(d as TimePicker)._blockManager.Render();
		}

		static object CoerceValue(DependencyObject d, object value)
		{
			return value;
		}

		internal DateTime InternalValue
		{
			get
			{
				DateTime? value = this.Value;
				if (value.HasValue) return value.Value;
				return DateTime.MinValue;
			}
		}

		public TimePicker()
		{
			this.Init();
			_blockManager = new BlockManager(this, this.FormatString);
		}

		private void Init()
		{
			this.Template = this.GetTemplate();
			this.ApplyTemplate();
			_checkBox = (CheckBox)this.Template.FindName("checkBox", this);
			_textBox = (TextBox)this.Template.FindName("textBox", this);
			_textBlock = (TextBlock)this.Template.FindName("textBlock", this);
			_calendar = new Calendar();
			_popUp = new Popup();
			_popUp.PlacementTarget = _textBox;
			_popUp.Placement = PlacementMode.Bottom;
			_popUp.Child = _calendar;
			_checkBox.Checked += _checkBox_Checked;
			_checkBox.Unchecked += _checkBox_Checked;
			this.MouseWheel += TimePicker_MouseWheel;
			this.Focusable = false;
			_textBox.Cursor = System.Windows.Input.Cursors.Arrow;
			_textBox.AllowDrop = false;
			_textBox.GotFocus += _textBox_GotFocus;
			_textBox.PreviewMouseUp += _textBox_PreviewMouseUp;
			_textBox.PreviewKeyDown += _textBox_PreviewKeyDown;
			_textBox.ContextMenu = null;
			_textBox.IsEnabled = this.Checked;
			_textBox.IsReadOnly = true;
			_textBox.IsReadOnlyCaretVisible = false;
			_textBlock.MouseLeftButtonDown += _textBlock_MouseLeftButtonDown;
			_calendar.SelectedDatesChanged += calendar_SelectedDatesChanged;
		}

		void _textBlock_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
		{
			_popUp.IsOpen = !(_popUp.IsOpen);
		}

		void _checkBox_Checked(object sender, RoutedEventArgs e)
		{
			_textBox.IsEnabled = this.Checked;
		}

		void TimePicker_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
		{
			_blockManager.Change(((e.Delta < 0) ? -1 : 1), true);
		}

		void _textBox_GotFocus(object sender, System.Windows.RoutedEventArgs e)
		{
			_blockManager.ReSelect();
		}

		void _textBox_PreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
		{
			_blockManager.ReSelect();
		}

		void _textBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
		{
			byte b = (byte)e.Key;

			if (e.Key == System.Windows.Input.Key.Left)
				_blockManager.Left();
			else if (e.Key == System.Windows.Input.Key.Right)
				_blockManager.Right();
			else if (e.Key == System.Windows.Input.Key.Up)
				_blockManager.Change(1, true);
			else if (e.Key == System.Windows.Input.Key.Down)
				_blockManager.Change(-1, true);
			if (b >= 34 && b <= 43)
				_blockManager.ChangeValue(b - 34);
			if (b >= 74 && b <= 83)
				_blockManager.ChangeValue(b - 74);
			if (e.Key != System.Windows.Input.Key.Tab)
				e.Handled = true;
		}

		void calendar_SelectedDatesChanged(object sender, SelectionChangedEventArgs e)
		{
			this.Checked = true;
			((sender as Calendar).Parent as Popup).IsOpen = false;
			DateTime selectedDate = (DateTime)e.AddedItems[0];
			this.Value = new DateTime(selectedDate.Year, selectedDate.Month, selectedDate.Day, this.InternalValue.Hour, this.InternalValue.Minute, this.InternalValue.Second);
			_blockManager.Render();
		}

		private ControlTemplate GetTemplate()
		{
			return (ControlTemplate)XamlReader.Parse(@"
        <ControlTemplate  xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                          xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
            <Border BorderBrush=""Black"" BorderThickness=""0"" CornerRadius=""1"">
                <StackPanel Orientation=""Horizontal"" VerticalAlignment=""Center"" HorizontalAlignment=""Left"" Background=""White"">
                    <CheckBox Name=""checkBox"" VerticalAlignment=""Center"" />
                    <TextBox Name=""textBox"" BorderThickness=""0""/>
                    <TextBlock Name=""textBlock"" Text=""▼""/>
                </StackPanel>
            </Border>
        </ControlTemplate>");
		}

		public override string ToString()
		{
			return this.InternalValue.ToString();
		}
	}

	internal class BlockManager
	{
		internal TimePicker _TimePicker2;
		private List<Block> _blocks;
		private string _format;
		private Block _selectedBlock;
		private int _selectedIndex;
		public event EventHandler NeglectProposed;
		private string[] _supportedFormats = new string[] { 
                "yyyy", "MMMM", "dddd", 
                "yyy", "MMM", "ddd", 
                "yy", "MM", "dd", 
                "y", "M", "d", 
                "HH", "H", "hh", "h",
                "mm", "m",
                "ss", "s",
                "tt", "t", 
                "fff", "ff", "f",
                "K", "g"};

		public BlockManager(TimePicker TimePicker2, string format)
		{
			_TimePicker2 = TimePicker2;
			_format = format;
			_TimePicker2.LostFocus += _TimePicker2_LostFocus;
			_blocks = new List<Block>();
			this.InitBlocks();
		}

		private void InitBlocks()
		{
			foreach (string f in _supportedFormats)
				_blocks.AddRange(this.GetBlocks(f));
			_blocks = _blocks.OrderBy(a => a.Index).ToList();
			_selectedBlock = _blocks[0];
			this.Render();
		}

		internal void Render()
		{
			int accum = 0;
			StringBuilder sb = new StringBuilder(_format);
			foreach (Block b in _blocks)
				b.Render(ref accum, sb);
			_TimePicker2._textBox.Text = _format = sb.ToString();
			this.Select(_selectedBlock);
		}

		private IEnumerable<Block> GetBlocks(string pattern)
		{
			List<Block> bCol = new List<Block>();

			int index = -1;
			while ((index = _format.IndexOf(pattern, ++index)) > -1)
				bCol.Add(new Block(this, pattern, index));
			_format = _format.Replace(pattern, (0).ToString().PadRight(pattern.Length, '0'));
			return bCol;
		}

		internal void ChangeValue(int p)
		{
			_selectedBlock.Proposed = p;
			this.Change(_selectedBlock.Proposed, false);
		}

		internal void Change(int value, bool upDown)
		{
			_TimePicker2.Value = _selectedBlock.Change(_TimePicker2.InternalValue, value, upDown);
			if (upDown)
				this.OnNeglectProposed();
			this.Render();
		}

		internal void Right()
		{
			if (_selectedIndex + 1 < _blocks.Count)
				this.Select(_selectedIndex + 1);
		}

		internal void Left()
		{
			if (_selectedIndex > 0)
				this.Select(_selectedIndex - 1);
		}

		private void _TimePicker2_LostFocus(object sender, RoutedEventArgs e)
		{
			this.OnNeglectProposed();
		}

		protected virtual void OnNeglectProposed()
		{
			EventHandler temp = this.NeglectProposed;
			if (temp != null)
			{
				temp(this, EventArgs.Empty);
			}
		}

		internal void ReSelect()
		{
			foreach (Block b in _blocks)
				if ((b.Index <= _TimePicker2._textBox.SelectionStart) && ((b.Index + b.Length) >= _TimePicker2._textBox.SelectionStart))
				{ this.Select(b); return; }
			Block bb = _blocks.Where(a => a.Index < _TimePicker2._textBox.SelectionStart).LastOrDefault();
			if (bb == null) this.Select(0);
			else this.Select(bb);
		}

		private void Select(int blockIndex)
		{
			if (_blocks.Count > blockIndex)
				this.Select(_blocks[blockIndex]);
		}

		private void Select(Block block)
		{
			if (_selectedBlock != block)
				this.OnNeglectProposed();
			_selectedIndex = _blocks.IndexOf(block);
			_selectedBlock = block;
			_TimePicker2._textBox.Select(block.Index, block.Length);
		}
	}

	internal class Block
	{
		private BlockManager _blockManager;
		internal string Pattern { get; set; }
		internal int Index { get; set; }
		private int _length;
		internal int Length
		{
			get
			{
				return _length;
			}
			set
			{
				_length = value;
			}
		}
		private int _maxLength;
		private string _proposed;
		internal int Proposed
		{
			get
			{
				string p = _proposed;
				return int.Parse(p.PadLeft(this.Length, '0'));
			}
			set
			{
				if (_proposed != null && _proposed.Length >= _maxLength)
					_proposed = value.ToString();
				else
					_proposed = string.Format("{0}{1}", _proposed, value);
			}
		}

		public Block(BlockManager blockManager, string pattern, int index)
		{
			_blockManager = blockManager;
			_blockManager.NeglectProposed += _blockManager_NeglectProposed;
			this.Pattern = pattern;
			this.Index = index;
			this.Length = this.Pattern.Length;
			_maxLength = this.GetMaxLength(this.Pattern);
		}

		private int GetMaxLength(string p)
		{
			switch (p)
			{
				case "y":
				case "M":
				case "d":
				case "h":
				case "m":
				case "s":
				case "H":
					return 2;
				case "yyy":
					return 4;
				default:
					return p.Length;
			}
		}

		private void _blockManager_NeglectProposed(object sender, EventArgs e)
		{
			_proposed = null;
		}

		internal DateTime Change(DateTime dateTime, int value, bool upDown)
		{
			if (!upDown && !this.CanChange()) return dateTime;
			int y = dateTime.Year;
			int m = dateTime.Month;
			int d = dateTime.Day;
			int h = dateTime.Hour;
			int n = dateTime.Minute;
			int s = dateTime.Second;

			if (this.Pattern.Contains("y"))
				y = ((upDown) ? dateTime.Year + value : value);
			else if (this.Pattern.Contains("M"))
				m = ((upDown) ? dateTime.Month + value : value);
			else if (this.Pattern.Contains("d"))
				d = ((upDown) ? dateTime.Day + value : value);
			else if (this.Pattern.Contains("h") || this.Pattern.Contains("H"))
				h = ((upDown) ? dateTime.Hour + value : value);
			else if (this.Pattern.Contains("m"))
				n = ((upDown) ? dateTime.Minute + value : value);
			else if (this.Pattern.Contains("s"))
				s = ((upDown) ? dateTime.Second + value : value);
			else if (this.Pattern.Contains("t"))
				h = ((h < 12) ? (h + 12) : (h - 12));

			if (y > 9999) y = 1;
			if (y < 1) y = 9999;
			if (m > 12) m = 1;
			if (m < 1) m = 12;
			if (d > DateTime.DaysInMonth(y, m)) d = 1;
			if (d < 1) d = DateTime.DaysInMonth(y, m);
			if (h > 23) h = 0;
			if (h < 0) h = 23;
			if (n > 59) n = 0;
			if (n < 0) n = 59;
			if (s > 59) s = 0;
			if (s < 0) s = 59;

			return new DateTime(y, m, d, h, n, s);
		}

		private bool CanChange()
		{
			switch (this.Pattern)
			{
				case "MMMM":
				case "dddd":
				case "MMM":
				case "ddd":
				case "g":
					return false;
				default:
					return true;
			}
		}

		public override string ToString()
		{
			return string.Format("{0}, {1}", this.Pattern, this.Index);
		}

		internal void Render(ref int accum, StringBuilder sb)
		{
			this.Index += accum;

			string f = _blockManager._TimePicker2.InternalValue.ToString(this.Pattern + ",").TrimEnd(',');
			sb.Remove(this.Index, this.Length);
			sb.Insert(this.Index, f);
			accum += f.Length - this.Length;

			this.Length = f.Length;
		}
	}
}
